class miniVue {
    constructor(options = {}) {
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
        this.$methods = options.methods;
        this.$data = options.data;
        this.$options = options;

        // data属性代理
        this.proxy(this.$data);
        // 响应式数据设置
        new Observer(this.$data);
        // 编译
        new Compiler(this);
    }

    // 代理属性（实现在vue中通过this.  获取到data中的属性）
    proxy(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                    return data[key];
                },

                set(newValue) {
                    if (newValue === data[key] || _isNaN(newValue, data[key])) {
                        return;
                    }
                    data[key] = newValue;
                }
            })
        })
    }
}

// 数据响应式观察者observer类
class Observer {
    constructor(data) {
        this.walk(data);
    }

    walk(data) {
        if (typeof data !== 'object' || !data) {
            return;
        }

        // 数据得每一个属性都调用定义响应式对象方法
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
        });
    }

    // 我们需要给每一个数据都添加响应式对象，并且转换成getter和setter函数，这里我们又用到了Object.defineProperty方法，我们需要在getter函数中收集依赖，在setter函数中发送通知，用来通知依赖进行更新，   此处我们用一个专门去指定定义响应式对象的方法  walk

    defineReactive(data, key, value) {
        // 获取当前this,以避免后续用vm的时候this指向不对
        const vm = this;
        // 递归调用walk方法，因为对象里边有可能还是对象
        this.walk(value);
        // 实例化收集依赖得类
        let dep = new Dep();
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 收集依赖，依赖存在Dep类上边
                Dep.target && dep.add(Dep.target);
                return value;
            },
            set(newValue) {
                if (newValue === value || _isNaN(value, newValue)) {
                    return;
                }
                value = newValue;
                // newValue也有可能是对象，所以递归
                vm.walk(newValue);
                // 通知Dep类  发送通知
                dep.notify();
            }
        })
    }
}

// 依赖类
class Dep {
    constructor() {
        this.deps = new Set();
    }

    add(dep) {
        // 判断dep是否存在并且是否存在update方法，然后存储到依赖的数据结构中
        if (dep && dep.update) {
            this.deps.add(dep);
        }
    }

    notify(newValue) {
        // 发布通知无非是遍历每一个dep，然后调用每一个dep的update方法，使得每一个依赖都回进行更新
        this.deps.forEach(dep => {
            dep.update(newValue);
        });
    }
}

class Watcher {
    constructor(vm, key, cb) {
        //构造函数内部
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        //依赖类
        Dep.target = this;
        // 我们用一个变量来存储旧值，也就是未变更之前的值
        this.__old = vm[key];
        Dep.target = null;
    }

    update() {
        //获取新的值
        let newValue = this.vm[this.key];
        //与旧值做比较，如果没有改变就无需执行下一步
        if (newValue === this.__old || _isNaN(newValue, this.__old)) return;
        //把新的值回调出去
        this.cb(newValue);
        //执行完之后，需要更新一下旧值的存储
        this.__old = newValue;
    }
}

class Compiler {
    constructor(vm) {
        // 跟元素
        this.el = vm.$el;
        // 事件方法
        this.methods = vm.$methods;
        // 当前组件实例
        this.vm = vm;
        // 调用编译函数开始编译
        this.compile(this.el);
    }

    compile(el) {
        // 获取所有的子节点（包含文本节点）
        let childNodes = el.childNodes;

        Array.from(childNodes).forEach(node => {
           // 判断是文本节点还是元素节点，然后分别调用对应的编译方法
            if (this.isTextNode(node)) {
                this.compileText(node);
            } else if (this.isElementNode(node)){
                this.compileElment(node);
            }

            // 递归判断node下是否还含有子节点，如果有的化递归编译
            if (node.childNodes && node.childNodes.length) {
                this.compile(node);
            }
        });
    }

    // 文本节点编译
    compileText(node) {
        // 定义正则，匹配{{}}中的值
        let reg = /\{\{(.+?)\}\}/g;
        let value = node.textContent;
        // 判断是否又{{}}
        if (reg.test(value)) {
            //拿到{{}}中的count,由于我们是匹配一个捕获组，所以我们可以根据RegExp类的$1属性来获取这个count
            let key = RegExp.$1.trim();
            node.textContent = value.replace(reg, this.vm[key]);
            // 如果更新了值，还要做更改
            new Watcher(this.vm, key, newValue => {
                node.textContent = newValue;
            });
        }
    }

    // 元素节点编译
    compileElment(node){
        // 指令就是一堆属性，我们只需获取属性即可
        const attrs = node.attributes;
        if (attrs.length) {
            Array.from(attrs).forEach(attr => {
                const nodeName = attr.nodeName;
                // 这里由于我们拿到的attributes可能包含不是指令的属性，所以我们需要先做一次判断
                if (this.isDirective(nodeName)) {
                    // 根据v-来截取一下后缀属性名，例如v-on:click,substr(5)即可截取到click，v-text与v-model则substr(2)截取到text和model即可
                    let attrName = nodeName.indexOf(':') > -1 ? nodeName.substr(5) : nodeName.substr(2);
                    let key = attr.value;
                    // 单独定义一个update方法来区分这些
                    this.update(node, attrName, key, this.vm[key]);
                }    
            })
            
        }
    }

    update(node, attrName, key, value) {
        if (attrName === 'text') {
            node.textContent = value;
            new Watcher(this.vm, key, newValue => {
                node.textContent = newValue;
            });
        } else if (attrName === 'model') {
            node.value = value;
            new Watcher(this.vm, key, newValue => {
                node.value = newValue;
            });
            node.addEventListener('input', (e) => {
                this.vm[key] = e.target.value;
            });
        } else if (attrName === 'click') {
            node.addEventListener(attrName, this.methods[key].bind(this.vm));
        }
    }

    // 文本节点判断
    isTextNode(node) {
        return node.nodeType === 3;
    }

    // 元素节点判断
    isElementNode(node) {
        return node.nodeType === 1;
    }

    // v- 指令判断
    isDirective(dir) {
        return dir.startsWith('v-');
    }
}

function _isNaN(a, b) {
    return Number.isNaN(a) && Number.isNaN(b);
}

export default miniVue;